# -*- coding:utf-8 -*- 

from cn.pigersing.PyShare.Event import *
from cn.pigersing.PyShare.PyShareEvents import *
from cn.pigersing.PyShare.Services.ServiceBase import *
from cn.pigersing.PyShare.Services.ServiceExceptions import *
from cn.pigersing.PyShare.utils import encoding
from cn.pigersing.PyShare.utils.LogPrint import get_logger
from cn.pigersing.PyShare.utils.UdpMCast import get_ip_address
from cn.pigersing.PyShare.utils.Xml2Obj import Element
from time import time
import StringIO
import cn.pigersing.PyShare.utils.utils as utils
import gzip
import hashlib
import os
import re
import socket
import threading
import urllib
import urllib2
import xmlrpclib
import zlib
import sys
if float(sys.version[:3]) > 2.5 :
    import tarfile
else :
    import cn.pigersing.tarfile as tarfile


logger = get_logger('FileShare')
# don't display the debug information
#logger.setLevel(20)

STATE_PERMISSION = - 2
STATE_FINISH = - 1
STATE_DENIE = 0
STATE_ABORT = 1
STATE_CANCEL = 2

FILE_SEP = os.path.sep

class FileRequest(Element):
    
    def __init__(self, file_path='', base_path='', zipped=False):
        
        super(FileRequest, self).__init__('FileRequest')
        
        self.file_id = ''
        self.file_name = ''
        self.file_size = - 1
        self.offset = 0
        self.zipped = zipped
        self.base_path = base_path
        self.file_path = file_path
    
    def file_path(): #@NoSelf
        doc = """File path of the request file""" #@UnusedVariable
       
        def fget(self):
            if self._file_path == '' :
                return os.path.join(self.base_path, self.relative_path)
            return self._file_path
        def fset(self, value):
            self._file_path = value
            if value != '' :
                if os.path.exists(value) :
                    # get the file information, if file_path is validate
                    if os.path.isdir(value) :
                        if not value.endswith(FILE_SEP) :
                            value += FILE_SEP
                    elif self.file_size < 0:
                        self.file_size = os.path.getsize(value)
                    if self.base_path == '' :
                        self.base_path = os.path.split(value)[0]
                    try :
                        path = value
                        self.relative_path = path[path.index(self.base_path) + len(self.base_path):]
                    except :
                        raise ValueError('Bad base path:%s' % self.base_path)
                    
            if self.file_id == '' :
                h = hashlib.md5()
                h.update(self.file_path)
                self.file_id = h.hexdigest()
        return locals()
       
    file_path = property(**file_path())
    
    def base_path(): #@NoSelf
       
        def fget(self):
            return self._base_path
        def fset(self, value):
            if value != '' and not value.endswith(FILE_SEP) :
                value += FILE_SEP
            self._base_path = value
        return locals()
       
    base_path = property(**base_path())
    
    def relative_path(): #@NoSelf
       
        def fget(self):
            return self.getAttribute('relative_path').replace('/', FILE_SEP)
        def fset(self, value):
            value = value.replace(FILE_SEP, '/')
            self.setAttribute('relative_path', value)
        return locals()
       
    relative_path = property(**relative_path())
    
    def file_id(): #@NoSelf
        doc = """File ID in provider's server""" #@UnusedVariable
       
        def fget(self):
            return self.getAttribute('file_id')
        def fset(self, value):
            self.setAttribute('file_id', value)
        return locals()
       
    file_id = property(**file_id())
    
    def file_name(): #@NoSelf
        doc = """File name""" #@UnusedVariable
       
        def fget(self):
            return os.path.split(self.relative_path)[ - 1]
        def fset(self, value):
            self.relative_path = os.path.join(os.path.split(self.relative_path)[0], value)
        return locals()
       
    file_name = property(**file_name())
    
    def file_size(): #@NoSelf
        doc = """File size""" #@UnusedVariable
               
        def fget(self):
            return long(self.getAttribute('file_size'))
        def fset(self, value):
            if isinstance(value, (int, long)):
                value = str(value)
            self.setAttribute('file_size', value)
        return locals()
       
    file_size = property(**file_size())
    
    def offset(): #@NoSelf
        doc = """Offset indicate from where to start transmission""" #@UnusedVariable
       
        def fget(self):
            return long(self.getAttribute('offset'))
        def fset(self, value):
            if isinstance(value, (int, long)):
                value = str(value)
            self.setAttribute('offset', value)
        return locals()
       
    offset = property(**offset())
    
    def zipped(): #@NoSelf
           
            def fget(self):
                return self.getAttribute('zipped') in ('true', 'True', '1')
            def fset(self, value):
                if isinstance(value, (int, long, bool)):
                    value = str(value).lower()
                self.setAttribute('zipped', value)
            return locals()
           
    zipped = property(**zipped())
    
    @property
    def is_dir(self):
        return not self.file_name
    
    @staticmethod
    def from_xml_string(src):
        element = Element.from_xml_string(src)
        file_info = FileRequest()
        # set properties
        file_info.file_id = element.getAttribute('file_id')
        file_info.file_name = element.getAttribute('relative_path')
        file_info.file_size = element.getAttribute('file_size') 
        file_info.offset = element.getAttribute('offset')
        file_info.zipped = element.getAttribute('zipped')
        return file_info


class FileTransportBase:
    
    chuck_size = 16 * 1024
    
    def __init__(self, session, fileinfo, buffer_size):
        self.session = session
        self.file = fileinfo
        self.buffer_size = buffer_size
        self.server = session
        self._abort = threading.Event()
        self._lock = threading.Condition(threading.Lock())
        self._begintime = - 1
        self._transportted = 0
    
    @property
    def id(self):
        return self.file.file_id
    
    @property
    def url(self):
        finame = self.file.relative_path.rstrip('/')
        if self.file.file_name and self.file.zipped :
             finame += '.gz'
        if self.file.is_dir :
            finame += '.tar'
        url = '%s/%s/file/%s/%s' % (self.server.url, self.server.id, self.file.file_id, urllib.quote(finame))
        return url
    
    @staticmethod
    def timing(meth):
        def _caller(*args, **dic_args):
            self = args[0]
            if self._begintime < 0:
                self._begintime = time()
            return meth(*args, **dic_args)
        
        return _caller
    
    def write_file(self, openfile, wfile, size= - 1):
        sent = 0
        try :
            while ((size < 0) or (sent < size)) and (not self._abort.isSet()):
                buffer = openfile.read(self.chuck_size)
                if not buffer : break
                wfile.write(buffer)
                s = len(buffer)
                sent += s
                self.stepped(s)
            # flushed the reminder
            wfile.flush()
        except Exception, why :
            logger.debug(str(why))
    
    def stop(self):
        if not self._abort.isSet() :
            self._abort.set()
        
    def stepped(self, length):
        # count the sent bytes
        self._lock.acquire()
        self._transportted += length
        # notify the client
        self.session.post_event(Evt_Transport_Stepped(file_request=self.file,
                                                                        offset=self._transportted,
                                                                        time=time() - self._begintime))
        self._lock.release()
        

class DecompressFileHelper:
    
    def __init__(self, filename, fileobj):
        self._buffer = StringIO.StringIO()
        self._decompressor = gzip.GzipFile(filename, 'rb', fileobj=self._buffer)
        self.fileobj = fileobj
        self.reach_EOF = False
        self._last_pos = 0
    
    def read(self, size):
         
        if not self.reach_EOF :
            buffer_size = size * 2
            data = self.fileobj.read(buffer_size)
            if not data :
                self.reach_EOF = True
            else:
                pos = self._buffer.tell()
                self._buffer.seek(self._last_pos)
                self._buffer.write(data)
                self._last_pos = self._buffer.tell()
                self._buffer.seek(pos)
        
        return self._decompressor.read(size)
    
    def close(self):
        self._buffer.close()
        self._decompressor.close()
        
        
class FolderHelper:

    def __init__(self, transportor, fileobj):
        self._transportor = transportor
        self._fileinfo = transportor.file
        self._fileobj = fileobj
        self._stepped = transportor.stepped
        self.init_extra()
    
    def init_extra(self): pass
    
    def start(self):
        raise NotImplementedError
    
    def stepped(self, length):
        if self._stepped :
            self._stepped(length)


class FolderReader(FolderHelper):
    
    def init_extra(self): 
        self.walker = self._make_walker(self._fileinfo.file_path)
        name = self._fileinfo.relative_path.strip(FILE_SEP)
        self.archive = tarfile.open(name, 'w|', self, self._transportor.chuck_size, format=tarfile.PAX_FORMAT)
        self.archive.posix = True
    
    def write(self, data):
        self._fileobj.write(data)
        self.stepped(len(data))
    
    def start(self):
        for filrqst in self.walker :
            if filrqst.is_dir :
                self._adddir(filrqst)
            else:
                self._addfile(filrqst)
        self.archive.close()
        
    def _adddir(self, filrqst):
        tarinfo = self.archive.gettarinfo(filrqst.file_path)
        tarinfo.name = filrqst.relative_path
        self.archive.addfile(tarinfo)
        
    def _addfile(self, filrqst):
        assert not filrqst.is_dir
        tarinfo = self.archive.gettarinfo(filrqst.file_path)
        tarinfo.name = filrqst.relative_path
        try :
            fileobj = open(filrqst.file_path, 'rb', self._transportor.buffer_size)
            self.archive.addfile(tarinfo, fileobj)
        finally :
            fileobj.close()    
    
    def _make_walker(self, base_path):
        for root, dirs, files in os.walk(base_path) :
            for file in files :
                name = os.path.join(root, file)
                yield FileRequest(name, base_path)
            for folder in dirs :
                name = os.path.join(root, folder)
                yield FileRequest(name, base_path)


class FolderWriter(FolderHelper): 

    def init_extra(self):
        pass
    
    def read(self, size):
        pass
    
    def start(self):
        pass
    

class FileProvider(FileTransportBase, PyShareEventHandleBase):
    
    def __init__(self, session, server, fileinfo, buffer_size=5 * 1024 ** 2):
        FileTransportBase.__init__(self, session, fileinfo, buffer_size)
        PyShareEventHandleBase.__init__(self)
        # register a event handler
        self.server = server
        server.add_handler(self)
        self.add_listener(Evt_GET_Task, self.handle_get)
        self.thread_safe = True
    
    @FileTransportBase.timing
    def handle_get(self, evt):
        
        if evt.task_id != self.id :
            return
        
        if self._abort.isSet() :
            return evt.request.report_404('File task was canceled')
        
        openfile = self.send_head(evt.request)
        wfile = evt.request.wfile
        if openfile:
            try :
                if self.file.zipped :
                    wfile = gzip.GzipFile(self.file.file_name, 'wb', compresslevel=1, fileobj=wfile)
                self.write_file(openfile, wfile)
            except Exception, why:
                logger.debug(str(why))
            finally :
                openfile.close()
        elif self.file.is_dir:
            try:
                FolderReader(self, wfile).start()
            finally:
                wfile.close()
        if evt.request.headers.get('User-Agent', '').find('PyShare') >= 0:
            if self._transportted >= self.file.file_size :
                self.session.post_event(Evt_Transport_Finish(file_request=self.file))
    
    def send_head(self, request):
        if self.file.is_dir :
            request.send_response(200)
            ctype = utils.guess_type('dummy.tar')
            request.send_header("Content-type", ctype)
            f = None
        else :
            path = self.file.file_path
            try:
                f = file(path, 'rb', self.buffer_size)
                size = self.file.file_size
            except IOError:
                request.report_404("File not found or this file has expired")
                return None
            
            if not self.file.zipped :
                datarange = self.parse_range(request.headers.get('Range', ''))
                if datarange:
                    request.send_response(206)
                    request.send_header("Content-Length", size)
                    request.send_header("Accept-Ranges", 'bytes')
                    request.send_header('Content-Range', '%d-%d/%d' % (datarange[0], datarange[1], size))
                    # set the file to the offset
                    f.seek(datarange[0])
                else:
                     request.send_response(200)
                     request.send_header("Content-Length", size)
                ctype = utils.guess_type(path)
                request.send_header("Content-type", ctype)
            else :
                 request.send_response(200)
                 ctype = utils.guess_type('dummy.gz')
                 request.send_header("Content-type", ctype)
            
            fs = os.fstat(f.fileno())
            request.send_header("Last-Modified", request.date_time_string(fs.st_mtime))
        request.end_headers()
        
        return f
    
    def parse_range(self, datarange) :
        datarange = re.findall('(\d*)', datarange)
        if datarange :
            datarange = filter(None, datarange)
            datarange = [ int(x) for x in datarange ]
            if len(datarange) == 2 :
                if not any([ x < 0 or x > self.file.file_size for x in datarange ])  :
                    return (min(datarange), max(datarange))
            elif len(datarange) == 1 :
                return (datarange[0], self.file.file_size)
        return None


class FileReceiver(FileTransportBase):
    
    def __init__(self, session, fileinfo, buffer_size=5 * 1024 * 5):
        FileTransportBase.__init__(self, session, fileinfo, buffer_size)
    
    @FileTransportBase.timing
    def recv(self):
        if self._abort.isSet() :
            logger.debug('File task was canceled')
            return 

        wfile, rfile = self.send_head(datarange)
        if wfile :
            try :
                self.write_file(rfile, wfile, size)
            finally :
                wfile.close()
                rfile.close()
        elif self.file.is_dir:
            try:
                FolderWriter(self.file.file_path, rfile, self).start()
            finally:
                wfile.close()
            
        if self._transportted >= self.file.file_size :
                self.session.post_event(Evt_Transport_Finish(file_request=self.file))
    
    def send_head(self):
        
        request = urllib2.Request(self.url)
        request.add_header('User-Agent', 'PyShare/v0.1')
        datarange = None
        size = self.file.file_size
        if self.file.offset > 0 :
            datarange = (self.file.offset, size)
            size = size - self.file.offset + 1
        if datarange :
            request.add_header('Range', 'bytes=%d-%d' % datarange)
        
        rfile = self.get_connection(request)
        if rfile :
            path = self.file.file_path
            if os.path.isdir(path) :
                wfile = None
            elif datarange and rfile.code == 206 :
                wfile = file(path, 'ab', self.buffer_size)
            else :
                wfile = file(path, 'w', self.buffer_size)
                if self.file.zipped :
                   rfile = DecompressFileHelper(self, rfile)
                   
            return wfile, rfile
        
        raise Exception('Can not get a connection to %s' % self.url)
            
    def get_connection(self, request):
        try :
            conn = urllib2.urlopen(request)
            return conn
        except urllib2.HTTPError, e:
            logger.debug('Error code: %d' % e.code)
        except Exception, e:
            logger.debug('URL error: %s' % str(e))
        
        return None
                

class FileShare(ServiceNode):
    
    BUFFER_SIZE = 1024 ** 2 * 10  # 10 MB
    CHUCK_SIZE = 1024 ** 2 # 1 MB
    
    def __init__(self):
        super(FileShare, self).__init__('file')
        
    @expose
    def set_state(self, id, file_id, state):
        session = self.root.server.get_session(id)
        if session :
            # state greater than 0  means stopping transmitting, so release the resources
            file_dic = session['file_transport']
            obj = file_dic[file_id]
            if isinstance(obj, tuple) :
                file_info = obj[0]
                stop = obj[1].set
            else:
                file_info = obj.file
                stop = obj.stop
            if state >= 0 :
                stop()
            # post event
            post_event(Evt_Transport_StateChanged(session=session, file_request=file_info, state=state))
            
    @expose
    def receive_data(self, id, file_request):
        # first verify the coming session
        session = self.root.server.get_session(id)
        if session :
            # its a valid session, dispatch it
            filrqst = FileRequest.from_xml_string(file_request)
            if not session['file_transport'] : session['file_transport'] = {}
            #
            # create start and deny and next closure
            def _start(dest_name):
                # notify the remote peer
                session.remote_server.file.set_state(self.root.server.id, filrqst.file_id, STATE_PERMISSION)
                if not filrqst.is_dir:
                    # receive a single file
                    filrqst.file_path = dest_name
                    return self._start(session, filrqst)
                elif not os.path.isfile(dest_name) :
                    # receive a folder
                    stop_evt = threading.Event()
                    file_dic = session['file_transport']
                    file_dic[filrqst.file_id] = (filrqst, stop_evt)
                    path = os.path.join(dest_name, filrqst.relative_path)
                    # create the root directory if not exists
                    if not os.path.exists(path) :
                        os.mkdir(path)
                    filrqst.base_path = path
                    return _get_next()
                
                else :
                    raise Exception('Invalid FileRequest %s' % file_request)
            
            def _deny():
                return self._deny_recv(session, filrqst.file_id)
            
            def _get_next(): 
                file_dic = session['file_transport']
                stop_evt = file_dic[filrqst.file_id][1]
                if not stop_evt.isSet():
                    id = filrqst.file_id
                    newfile = session.remote_server.file.get_next(self.root.server.id, id)
                    if newfile :
                        newfile = FileRequest.from_xml_string(newfile)
                        newfile.base_path = filrqst.base_path
                        self._start(session, newfile)
                        return newfile
                    else :
                        session.remote_server.file.set_state(self.root.server.id, filrqst.file_id, STATE_FINISH)
                        session.post_event(Evt_Transport_Finish(file_request=filrqst))
                return None
            
            if filrqst.is_dir :
                get_next = _get_next
            else:
                get_next = None
            
            # post file request event to listener
            post_event(Evt_File_Request(session=session,
                                                        file_request=filrqst,
                                                        is_folder=not(get_next is None),
                                                        start=_start,
                                                        deny=_deny,
                                                        next=get_next))
        else :    
            raise SessionInvalidError()
    
    
    @expose
    def get_next(self, id, folder_id): 
        session = self.root.server.get_session(id)
        if session : 
            file_dic = session['file_transport']
            if not file_dic : raise Exception('This session does not have data transmission task')
            
            try :
                folder, stop_evt, next_gen = file_dic[folder_id]
            except Exception, why :
                raise Exception('we dose not have a folder of id %s' % folder_id)
            
            if not(stop_evt and not stop_evt.isSet()) :
                return None
            
            try :
                file_info = next_gen.next()
                file_dic[file_info.file_id] = FileProvider(session, self.root.server, file_info) #  (file_info, stop_evt)
                session.post_event(Evt_Transport_FolderNext(file_request=file_info))
                return file_info.to_xml()
            except StopIteration:
                stop_evt.set()
                return None
    
    
    def _start(self, session, filrqst):
        
        fr = FileReceiver(session, filrqst)
        file_dic = session['file_transport']
        file_dic[filrqst.file_id] = fr
        # validate the directory
        dir = os.path.dirname(filrqst.file_path)
        if not os.path.exists(dir) : os.mkdir(dir)
        # start the receiving thread
        t = threading.Thread(target=fr.recv)
        t.start()
        
            
    def _deny_recv(self, session, file_id):
        session.remote_server.file.set_state(self.root.server.id, file_id, STATE_DENIE)
        
        
class FileShareClientBase(PyShareEventHandleBase):
    
    def __init__(self, buffer_size=(1024 ** 2) / 2):
        PyShareEventHandleBase.__init__(self)
        #
        self.buffer_size = buffer_size
        self.add_listener(Evt_File_Request, self.handler_recv)
        self.add_listener(Evt_Transport_StateChanged, self.on_statechange)

    
    def on_statechange(self, evt):
        
        filrqst = evt.file_request
        session = evt.session
        state = evt.state
        
        if state in (STATE_DENIE, STATE_FINISH) :
            # when on these states, delete the file information from session,
            # TODO: the other situation should be dealt in a loop checking
            file_dic = session['file_transport']
            del file_dic[filrqst.file_id]
            logger.debug('%s\'s information has been deleted : %s' % (filrqst.file_id, not file_dic.has_key(filrqst.file_id)))
        
        if state == STATE_DENIE :
            # emit a deny event
            EvtClass = Evt_Transport_Deny
            
        if state == STATE_FINISH :
            EvtClass = Evt_Transport_Finish
        
        if state == STATE_PERMISSION :
            EvtClass = Evt_Transport_Permission
            
        if state == STATE_ABORT :
            EvtClass = Evt_Transport_Abort
             
        if state == STATE_CANCEL :
             EvtClass = Evt_Transport_Cancel
             
        session.post_event(EvtClass(file_request=filrqst))
        
        logger.debug('state changed and emitted event : %s' % state)
            
    
    def handler_recv(self, evt):
        raise NotImplementedError


    def send_file(self, file_path, dst_id):
        if not os.path.exists(file_path) :
            raise  ValueError('The file of "%s" is invalid' % file_path)
        
        file_info = FileRequest(file_path)
        stop_evt = threading.Event()
        
        return self._send(dst_id, file_info)
    
    
    def send_folder(self, path, dst_id):
        if not os.path.isdir(path):
            raise  ValueError('"%s" is not a folder' % path)
        
        if not path.endswith(FILE_SEP) :
            path += FILE_SEP
        base = path[:path[: - 1].rindex(FILE_SEP)]
        
        file_info = FileRequest(path, base)
        stop_evt = threading.Event()
        walking = self._make_walker(path)
        
        return self._send(dst_id, file_info, walking)
    
    
    def _send(self, dst_id, file_info, walking=None):
        session = self.server.get_session(dst_id)
        if not session :
            raise Exception('destination peer dose not exist')
        
        if dst_id == self.server.id :
            raise Exception('Can not send to self')
        
        logger.debug('prepare to send : %s' % file_info.file_path)
        # for thread safety get the file information first
         
        if not session['file_transport'] : session['file_transport'] = {}
        
        file_dic = session['file_transport']
        if walking :
            file_dic[file_info.file_id] = (file_info, threading.Event(), walking)
            folder = FileProvider(session, self.server, file_info)
            file_info.url = folder.url
            logger.debug(folder.url)
        else :
#            file_info.zipped = True
            dataprovider = FileProvider(session, self.server, file_info)
            file_dic[file_info.file_id] = dataprovider
            file_info.url = dataprovider.url
            logger.debug(dataprovider.url)
        
        try :
            # notify remote server to receive file
            session.remote_server.file.receive_data(self.server.id, file_info.to_xml())

            
        except Fault, f:
            if f.faultCode != SESSION_INVAID : raise
            else : pass
                # we havn't joined to that peer, or we lost connection once
                #TODO: Correct the Event Handle
#                dispatch_event(Event('Auth.InvalidSession'
#                                     (session,)))
        
        return file_info
        
    
    def _make_walker(self, base_path):
        for root, dirs, files in os.walk(base_path) :
            for file in files :
                name = os.path.join(root, file)
                yield FileRequest(name, base_path)
    
    
#===============================================================================
#  test code
#===============================================================================


if __name__ == '__main__' :
    
    fp = FileRequest(__file__)
    print fp
    print fp.file_name
    print fp.file_path
    print fp.file_size
    print fp.offset
    print fp.relative_path
    print fp.base_path
    
    fp2 = FileRequest()
    print fp2
    fp3 = FileRequest.from_xml_string(fp.to_xml())
    fp3.base_path = '/home/ant-man/Downloads'
    fp3.file_name = 'renamed.filetype'
    print fp3.file_path
    print fp3
    fp4 = FileRequest('/home/ant-man/DownLoads/ILETS/', base_path='/home/ant-man/DownLoads')
    print FileRequest.from_xml_string(fp4.to_xml())
    print fp4.file_name or 'It is a folder'
    print fp4.is_dir
    